package no.uib.hplims.tools;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import no.uib.hplims.models.Role;
import no.uib.hplims.models.User;

import org.apache.commons.codec.binary.Base64;
import org.vaadin.appfoundation.authentication.exceptions.AccountLockedException;
import org.vaadin.appfoundation.authentication.exceptions.InvalidCredentialsException;
import org.vaadin.appfoundation.authentication.exceptions.PasswordsDoNotMatchException;
import org.vaadin.appfoundation.authentication.exceptions.UsernameExistsException;
import org.vaadin.appfoundation.authentication.util.AuthenticationUtil;
import org.vaadin.appfoundation.authentication.util.PasswordUtil;
import org.vaadin.appfoundation.persistence.facade.FacadeFactory;

public class SecurityUtil implements Serializable {

	private static final long serialVersionUID = 2026369494834863429L;

	public static void authenticateUser(String username, String password)
			throws InvalidCredentialsException, AccountLockedException {
		User user = (User) getUserForUsername(username);
		String salt = user.getPasswordSalt();

		byte[] saltAsByte = base64ToByte(salt);
		byte[] proposedDigest = getHash(password, saltAsByte);

		String proposedPassword = byteToBase64(proposedDigest);

		AuthenticationUtil.authenticate(username, proposedPassword);
	}

	public static User createUser(String username, String password,
			String verifyPassword, String name, String email, Role role)
			throws UsernameExistsException, PasswordsDoNotMatchException {

		Collection<Role> roles = new ArrayList<Role>();
		roles.add(role);

		return createUser(username, password, verifyPassword, name, email,
				roles);
	}

	public static User createUser(String username, String password,
			String verifyPassword, String name, String email,
			Collection<Role> roles) throws UsernameExistsException,
			PasswordsDoNotMatchException {

		User user = null;

		if (username != null && password != null && verifyPassword != null) {

			verifyUsernameAvailability(username);
			checkPasswordVerification(password, verifyPassword);

			String hashedPassword = null;
			String hashedVerifyPassword = null;
			String salt = null;

			// Use a secure Random
			SecureRandom random;
			try {
				random = SecureRandom.getInstance("SHA1PRNG");
				// Salt generation, 64 bits long
				byte[] byteSalt = new byte[8];
				random.nextBytes(byteSalt);
				// Digest
				byte[] bytePassword = getHash(password, byteSalt);
				byte[] byteVerifyPassword = getHash(verifyPassword, byteSalt);

				hashedPassword = byteToBase64(bytePassword);
				hashedVerifyPassword = byteToBase64(byteVerifyPassword);
				salt = byteToBase64(byteSalt);

				if (hashedPassword.equals(hashedVerifyPassword)) {
					user = new User();
					user.setUsername(username);
					user.setPassword(PasswordUtil
							.generateHashedPassword(hashedPassword));
					user.setPasswordSalt(salt);
					user.setName(name);
					user.setEmail(email);
					for (Role r : roles) {
						user.addRole(r);
					}
					FacadeFactory.getFacade().store(user);
				}
			} catch (NoSuchAlgorithmException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return user;
	}

	public static void deleteUser(User user) {
		FacadeFactory.getFacade().delete(user);
	}

	public static void changeUsername(User user, String username)
			throws UsernameExistsException {
		// check if the username is supposed to change
		if (user.getUsername().equals(username)) {
			return; // no change
		}
		// check if new username is available
		verifyUsernameAvailability(username);
		// set new username
		user.setUsername(username);
		FacadeFactory.getFacade().store(user);
	}

	/**
	 * Method to let users change their own passwords. A new password, and a new
	 * salt value are assigned to the user.
	 * 
	 * @param user
	 *            The user to change password for
	 * @param currentPassword
	 *            User's current password
	 * @param password
	 *            New password
	 * @param verifyPassword
	 *            Repeat new password
	 * @throws InvalidCredentialsException
	 * @throws PasswordsDoNotMatchException 
	 */
	public static void changePassword(User user, String currentPassword,
			String password, String verifyPassword)
			throws InvalidCredentialsException, PasswordsDoNotMatchException {
		// Current password
		String currentSalt = user.getPasswordSalt();
		byte[] saltAsByte = base64ToByte(currentSalt);
		byte[] currentPasswordDigest = getHash(currentPassword, saltAsByte);
		String currentPasswordHashed = byteToBase64(currentPasswordDigest);

		String hashedNewPassword = null;
		String hashedNewVerifyPassword = null;
		String newSalt = null;

		// Use a secure Random
		SecureRandom random;
		try {
			random = SecureRandom.getInstance("SHA1PRNG");
			// Salt generation, 64 bits long
			byte[] byteSalt = new byte[8];
			random.nextBytes(byteSalt);
			// Digest
			byte[] bytePassword = getHash(password, byteSalt);
			byte[] byteVerifyPassword = getHash(verifyPassword, byteSalt);

			hashedNewPassword = byteToBase64(bytePassword);
			hashedNewVerifyPassword = byteToBase64(byteVerifyPassword);
			newSalt = byteToBase64(byteSalt);

			if (PasswordUtil.verifyPassword(user, currentPasswordHashed)) {
				if (hashedNewPassword.equals(hashedNewVerifyPassword)) {
					user.setPassword(PasswordUtil
							.generateHashedPassword(hashedNewPassword));
					user.setPasswordSalt(newSalt);
					FacadeFactory.getFacade().store(user);
				} else {
					throw new PasswordsDoNotMatchException();
				}
			} else {
				throw new InvalidCredentialsException();
			}

		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Method to let admin users change a user's password. A new password, and a
	 * new salt value are assigned to the user.
	 * 
	 * @param user
	 *            The user to change password for
	 * @param password
	 *            New password
	 * @param verifyPassword
	 *            Repeat password
	 * @throws PasswordsDoNotMatchException
	 */
	public static void adminChangePassword(User user, String password,
			String verifyPassword) throws PasswordsDoNotMatchException {

		checkPasswordVerification(password, verifyPassword);

		String hashedPassword = null;
		String hashedVerifyPassword = null;
		String salt = null;

		// Use a secure Random
		SecureRandom random;
		try {
			random = SecureRandom.getInstance("SHA1PRNG");
			// Salt generation, 64 bits long
			byte[] byteSalt = new byte[8];
			random.nextBytes(byteSalt);
			// Digest
			byte[] bytePassword = getHash(password, byteSalt);
			byte[] byteVerifyPassword = getHash(verifyPassword, byteSalt);

			hashedPassword = byteToBase64(bytePassword);
			hashedVerifyPassword = byteToBase64(byteVerifyPassword);
			salt = byteToBase64(byteSalt);

			if (hashedPassword.equals(hashedVerifyPassword)) {
				user.setPassword(PasswordUtil
						.generateHashedPassword(hashedPassword));
				user.setPasswordSalt(salt);
				FacadeFactory.getFacade().store(user);
			}
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Check if password conforms to password requirements
	 * 
	 * @param password
	 *            The password to check
	 * @return True if password is valid
	 */
	public static boolean passwordIsValid(String password) {
		return PasswordUtil.isValid(password);
	}

	/**
	 * Check if new passwords match
	 * 
	 * @param password
	 *            New password
	 * @param verifyPassword
	 *            Verify password
	 * @return True if equal
	 */
	private static void checkPasswordVerification(String password,
			String verifyPassword) throws PasswordsDoNotMatchException {
		if (!password.equals(verifyPassword)) {
			throw new PasswordsDoNotMatchException();
		}
	}

	public static byte[] getHash(String password, byte[] salt) {

		try {
			MessageDigest digestor = MessageDigest.getInstance("SHA-512");
			digestor.reset();
			digestor.update(salt);
			byte[] input;
			input = digestor.digest(password.getBytes("UTF-8"));
			for (int i = 0; i < 1300; i++) {
				input = digestor.digest(input);
			}
			return input;
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * From a base 64 representation, returns the corresponding byte[]
	 * 
	 * @param data
	 *            String The base64 representation
	 * @return byte[]
	 */
	public static byte[] base64ToByte(String data) {
		Base64 decoder = new Base64();
		return decoder.decode(data);
	}

	/**
	 * From a byte[] returns a base 64 representation
	 * 
	 * @param data
	 *            byte[]
	 * @return String
	 */
	public static String byteToBase64(byte[] data) {
		Base64 encoder = new Base64();
		return encoder.encodeToString(data);
	}

	/**
	 * Fetches the User object from the database for the given username
	 * 
	 * @param username
	 * @return User object for username
	 */
	private static User getUserForUsername(String username) {
		// Create a query which searches the database for a user with the given
		// username
		String query = "SELECT u FROM User u WHERE u.username = :username";
		Map<String, Object> parameters = new HashMap<String, Object>();
		parameters.put("username", username);
		return (User) FacadeFactory.getFacade().find(query, parameters);
	}

	/**
	 * Verifies that the given username doesn't exist
	 * 
	 * @param username
	 *            The username to check
	 * @throws UsernameExistsException
	 *             Thrown if username already exists
	 */
	private static void verifyUsernameAvailability(String username)
			throws UsernameExistsException {
		if (!checkUsernameAvailability(username)) {
			throw new UsernameExistsException();
		}
	}

	/**
	 * Checks if the given username is available.
	 * 
	 * @param username
	 *            Desired username
	 * @return Returns true if the username doesn't exist, false if it exists
	 */
	private static boolean checkUsernameAvailability(String username) {
		String query = "SELECT u FROM User u WHERE u.username = :username";
		Map<String, Object> parameters = new HashMap<String, Object>();
		parameters.put("username", username);

		User user = FacadeFactory.getFacade().find(query, parameters);

		return user != null ? false : true;
	}

}
